using System.Collections.Generic;
using NyMPH.Persistence;
using NyMPH.Patches;
using System.Text;
using System;

public class PatchBank : IPersistence, IEquatable<PatchBank>, IComparable<PatchBank>
{
    private int _OID;
    private string _name;
    private List<Patch> _patches;
    private int _bankNumber;

    #region Properties

    /// <summary>
    /// The unique ObjectID within the NyMPH Patch table.
    /// </summary>
    public int OID
    {
        set
        {
            _OID = value;
           SetModified();
        }
        get { return _OID; }
    }

    private void SetModified()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// The Name of this Patch Bank
    /// </summary>
    public string Name
    {
        set
        {
            _name = value;
           SetModified();
        }
        get { return _name; }
    }

    /// <summary>
    /// A read-only List of Patch objects contained in this Patch Bank. This prevents direct manipulation of the List.
    /// </summary>
    public List<Patch> Patches
    {
        get
        {
            //TODO: Fix this
            System.Collections.ObjectModel.ReadOnlyCollection<Patch> _roPatches = _patches.AsReadOnly();

            return new List<Patch>();

        }
    }

    /// <summary>
    /// The Number assigned to this Patch Bank (1 - 9). This can be set to 0 if the Bank Number is not known. Attempting to set it to any value other than 0 - 9 will throw an exception
    /// </summary>
    /// <exception cref="ArgumentOutOfRangeException">Setting this property to a value other than 0 - 9 will result in an exception being thrown.</exception>
    public int BankNumber
    {
        set
        {
            if ((value >= 0) && (value <= 9))
            {
                _bankNumber = value;
               SetModified();
            }
            else
            {
                throw new System.ArgumentOutOfRangeException("BankNumber", value,
                    "The program attempted to set this property to an invalid value. The Bank Number must be between 1 and 9 inclusive. 0 is allowed as a null value.");
            }
        }
        get { return _bankNumber; }
    }
    #endregion

    #region Methods

    /// <summary>
    /// Adds an instance of Patch to this PatchBank. This overload will place it in the next available slot and return the number of the slot.
    /// </summary>
    /// <param name="NewPatch">A Patch to add to this PatchBank</param>
    /// <returns>The Slot (1 - 99) the Patch was added to. This will be 0 if a problem occurs.</returns>
    public int AddPatch(Patch NewPatch)
    {
        //find the first 'empty' slot in the list and stick the patch in there
        int i = _patches.IndexOf(null, 0);

        //call overload
        AddPatch(NewPatch, i + 1);

        return i;
    }

    /// <summary>
    /// Adds an instance of Patch to this PatchBank in the specified Slot (1 - 99).
    /// </summary>
    /// <param name="NewPatch">A Patch to add to this PatchBank. A null Patch will not be added.</param>
    /// <param name="Slot">A Slot number (1 - 99) to put the Patch in.</param>
    /// <exception cref="ArgumentOutOfRangeException">The Slot number MUST be 1 - 99.</exception>
    public void AddPatch(Patch NewPatch, int Slot)
    {
        if ((Slot < 1) || (Slot > 99))
        {
            throw new ArgumentOutOfRangeException("Slot", Slot, "Invalid Slot number; must be between 1 and 99 inclusive.");
        }
        else
        {
            if (NewPatch != null)
            {
                //convert Slot to Index and assign patch
                _patches[Slot - 1] = NewPatch;
               SetModified();
            }
        }
    }

    /// <summary>
    /// Removes an instance of Patch from this PatchBank. This overload will remove the first instance it finds.
    /// </summary>
    /// <param name="_patch">A Patch to remove from this PatchBank</param>
    /// <returns>The Slot (1 - 99) the Patch was removed from. This will be 0 if a problem occurs.</returns>
    public int RemovePatch(Patch _patch)
    {
        //get the index of the matching _patch and set it to null
        int i = _patches.IndexOf(_patch, 0);

        _patches[i] = null;
       SetModified();

        return i;
    }

    /// <summary>
    /// Removes an instance of Patch from this PatchBank at the specified Slot (1 - 99).
    /// </summary>
    /// <param name="Slot">A Slot number (1 - 99) to remove the Patch from.</param>
    /// <exception cref="ArgumentOutOfRangeException">The Slot number MUST be 1 - 99.</exception>
    public void RemovePatch(int Slot)
    {
        if ((Slot < 1) || (Slot > 99))
        {
            throw new ArgumentOutOfRangeException("Slot", Slot, "Invalid Slot number; must be between 1 and 99 inclusive.");
        }
        else
        {
            //convert Slot to Index and set that index to null
            Patches[Slot - 1] = null;
           SetModified();
        }
    }

    /// <summary>
    /// Gets the Patch object corresponding to the specified Slot
    /// </summary>
    /// <param name="Slot">A Slot number (1 - 99) to get the Patch from.</param>
    /// <returns>The Patch instance stored in the Slot provided. This may return null.</returns>
    public Patch GetPatch(int Slot)
    {
        if ((Slot < 1) || (Slot > 99))
        {
            throw new ArgumentOutOfRangeException("Slot", Slot, "Invalid Slot number; must be between 1 and 99 inclusive.");
        }
        else
        {
            //todo: May want to return an 'empty' instance of Patch instead of a null
            //convert Slot to Index and return the Patch object
            return Patches[Slot - 1];
        }
    }

    /// <summary>
    /// Gets the first Patch object that matches the Name
    /// </summary>
    /// <param name="PatchName">The Name of the Patch to retrieve. This must not be null.</param>
    /// <returns>The first Patch instance with the same Name. This may return null.</returns>
    public Patch GetPatch(string PatchName)
    {
        //Test for null pathname, then find Patch object using predicate that matches on Name
        return string.IsNullOrEmpty(PatchName) ? null : Patches.Find(x => x.Name == PatchName);
    }

    /// <summary>
    /// Create all of the Patches contained in this PatchBank to the location provided. This will also create a PatchList file (*.pchList)
    /// </summary>
    /// <param name="Location">The filesystem location to export the Patches to.</param>
    /// <returns>True if the operation succeeded, otherwise false.</returns>
    public bool Export(string Location)
    {
        // TODO: This is over-simplified for now; the exception handling should be more detailed, and verifying the files have been written would be good.

        try
        {
            foreach (Patch p in this._patches)
            {
                p.Data.Location = Location;
                p.Data.CreateFile();
            }

            //create patch list file
            CreatePatchListFile(Location);

            return true;
        }
        catch (Exception)
        {
            return false;
        }

    }

    /// <summary>
    /// Create a Patch List for this Patch Bank instance and write it out to the given location. The Name of the Patch Bank will be used for the file name.
    /// </summary>
    /// <param name="Location">The directory to save the Patch List file in.</param>
    /// <returns>True if the Patch List was successfully written, otherwise false.</returns>
    public bool CreatePatchListFile(string Location)
    {
        //init string builder to 2082 chars, which should be the maximum (two chars + crlf for bank number, then 99 x 21 [16 chars + .pch + crlf] chars for filenames 
        StringBuilder _sb = new StringBuilder(2082);

        _sb.AppendLine(this.BankNumber.ToString());

        foreach (Patch p in this.Patches)
        {
            //append file extension and crlf to Patch Name
            _sb.AppendFormat("{0}.pch\n", p.Name);
        }

        //write out to file and return result using Patch Bank name for a file name.
        return FileSystem.Write(String.Format(@"{0}\{1}.pchList", Location, this.Name), _sb.ToString());

    }

    /// <summary>
    /// Returns the Name of this Patch Bank instance.
    /// </summary>
    /// <returns>The value of the Name property.</returns>
    public override string ToString()
    {
        return this.Name;
    }

    #endregion

    #region IPersistence Members

    private bool _hasChanges;

    public bool Save()
    {
        throw new System.NotImplementedException();
    }

    public bool Load()
    {
        throw new System.NotImplementedException();
    }

    public bool Delete()
    {
        throw new System.NotImplementedException();
    }

    public int GetOID()
    {
        throw new System.NotImplementedException();
    }

    public void Find(Criteria item)
    {
        throw new System.NotImplementedException();
    }

    public bool HasChanges()
    {
        throw new System.NotImplementedException();
    }

    public void HasChanges(bool Changed)
    {
        throw new System.NotImplementedException();
    }

    #endregion

    #region IEquatable<PatchBank> Members
    /// <summary>
    /// Determines whether this instance of PatchBank and a specified object, which must also be a PatchBank object, have the same values.
    /// </summary>
    /// <param name="Obj">The PatchBank object to compare values with</param>
    /// <returns>True if all properties have matching values, otherwise false.</returns>
    /// <remarks></remarks>
    public bool Equals(PatchBank Obj)
    {
        //AND all equality evaluations together
        //Todo: Is it necessary to compare Patch lists? If so, perhaps make a PatchCollection class?

        //compare 'easy' ones first to conserve resources; if any of the name, bank, and number of patches are different it is not equal and we can skip the enumeration

        //only count once for each collection
        int _myCount = this.Patches.Count;
        bool _isEqual = (this.Name == Obj.Name && this.BankNumber == Obj.BankNumber && _myCount == Obj.Patches.Count);

        if (_isEqual)
        {
            int i = 0;
            while (_isEqual && i < _myCount)
            {
                //compare each element until an inequality is found
                _isEqual = this.Patches[i].Equals(Obj.Patches[i]);
            }

            //will stay 'true' until an inequality is detected.
            return _isEqual; 
        }

        else
        {
            return false;
        }
    }

    /// <summary>
    /// Determines whether this instance of PatchBank and a specified object, which must also be a PatchBank object, have the same values.
    /// </summary>
    /// <param name="Obj">The PatchBank to compare values with</param>
    /// <returns>True if all properties have matching values, otherwise false.</returns>
    /// <remarks>Overrides the Object.Equals(object) method. This ensures all invocations of Equals() for the PatchBank type return the same result.</remarks>
    public override bool Equals(object Obj)
    {
        //use base class method for comparing nulls; casting them would throw an exception
        if (Obj == null) return base.Equals(Obj);

        //make sure object can be cast to PatchBank and call the Equals(PatchBank) method
        if (Obj is PatchBank)
        {
            return Equals((PatchBank)Obj);
        }
        else
        {
            throw new InvalidCastException("The 'Obj' argument is not a PatchBank object.");
        }
    }

    /// <summary>
    /// Provides a hash value based on XORing the Name and BankNUmber properties together along with the number of Patches it holds. Two PatchBank objects with the same values will produce the same hash code, although it isn't guaranteed to be unique
    /// </summary>
    /// <returns>The hash created from (Name xor BankNumber xor Patches.Count)</returns>
    /// <remarks>MSDN recommends overriding this method when implementing IEquatable. </remarks>
    public override int GetHashCode()
    {
        return this.Name.GetHashCode() ^ this.BankNumber ^ this.Patches.Count;
    }

    /// <summary>
    /// Create equality operator for PatchBank objects; allows expressions like {if (PatchBankA = PatchBankB)} to compare each member value instead of the object reference
    /// </summary>
    public static bool operator ==(PatchBank PatchBankA, PatchBank PatchBankB)
    {
        return PatchBankA.Equals(PatchBankB);
    }

    /// <summary>
    /// Create inequality operator for PatchBank objects; allows expressions like {if (PatchBankA != PatchBankB)} to compare each member value instead of the object reference
    /// </summary>
    public static bool operator !=(PatchBank PatchBankA, PatchBank PatchBankB)
    {
        return !PatchBankA.Equals(PatchBankB);
    }

    #endregion

    #region IComparable<PatchBank> Members

    /// <summary>
    /// This is required to implement IComparable, which is used when sorting PatchBanks. These are sorted by Name, then BankNumber
    /// </summary>
    /// <param name="other">The PatchBank object to compare values with</param>
    /// <returns>Less than 0 if 'other' PatchBank should be earlier in the sort order, 0 if equal, and greater than zero if the 'other' PatchBank should come after this PatchBank.</returns>
    /// <remarks></remarks>
    public int CompareTo(PatchBank other)
    {
        //compare by name with the Bank Number appended to it.
        return string.Compare(this.Name + this.BankNumber.ToString(), other.Name + other.BankNumber.ToString(), StringComparison.OrdinalIgnoreCase);
    }

    #endregion
}
